package bombita.modelo.Mapa;

import java.awt.Point;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;

import org.jdom2.Element;

import ar.uba.fi.algo3.titiritero.vista.CentralVista;
import bombita.modelo.CosaDelMapa;
import bombita.modelo.CosaDelMapaSincronica;
import bombita.modelo.Obstaculos.NoPasarException;
import bombita.vista.VistaMapa;

public class Mapa
{
	private int maxX;
	private int maxY;
	private Map<Point, LinkedList<CosaDelMapa>> contenido;
	private List<CosaDelMapaSincronica> contenidoSincronico;
	private List<CosaDelMapa> aEliminar;
	private ArrayList<VistaMapa> vistas;
	private boolean termino;
	private boolean ganado;
	public final static int pulsosPorSegundo = 10;

	public Mapa (int x, int y)
	{
		maxX = x;
		maxY = y;
		contenido = new HashMap<Point, LinkedList<CosaDelMapa>> ();
		contenidoSincronico = new CopyOnWriteArrayList<CosaDelMapaSincronica>();
		aEliminar = new ArrayList<CosaDelMapa>();
		vistas = new ArrayList<VistaMapa>();
		termino = false;
		ganado = false;
	}
	
	@Override
	public void finalize()
	{
		contenido = null;
		contenidoSincronico = null;
		aEliminar = null;
	}
	
	public int maxX() {
		return maxX;
	}
	
	public int maxY() {
		return maxY;
	}
	
	@SuppressWarnings("unchecked")
	public List<CosaDelMapa> cosasEnPosicion (Point posicion) throws PosicionFueraDeMapaException
	{
		if (fueraDeMapa(posicion)) {
			throw new PosicionFueraDeMapaException();
		} else {
			LinkedList<CosaDelMapa> encontrado = contenido.get(posicion); 
			if (encontrado == null) {
				return new LinkedList<CosaDelMapa>();
			} else {
				return (List<CosaDelMapa>) encontrado.clone();
			}
		}
	}
	
	public boolean fueraDeMapa (Point posicion)
	{
		if (1 <= posicion.x && posicion.x <= maxX && 1 <= posicion.y && posicion.y <= maxY) {
			return false;
		}
		return true;
	}
	
	public void reconocerSincronico (CosaDelMapaSincronica entidad, Point posicion) throws PosicionFueraDeMapaException
	{
		this.reconocer(entidad, posicion);
		contenidoSincronico.add(entidad);
	}
	
	public void reconocer (CosaDelMapa entidad, Point posicion) throws PosicionFueraDeMapaException
	{
		if (fueraDeMapa(posicion)) {
			throw new PosicionFueraDeMapaException();
		} else {
			entidad.setPosicion(new TokenMapa(this, entidad, posicion));
			if (!contenido.containsKey(posicion)) {
				contenido.put(posicion, new LinkedList<CosaDelMapa>());
			}
			contenido.get(posicion).add(entidad);
			for (VistaMapa vista : vistas) {
				vista.agregar(entidad);
			}
		}
	}
	
	/** elimina cosas muertas que siguen en el mapa */
	public void limpiar ()
	{
		for (CosaDelMapa cosa : aEliminar) {
			contenido.get(cosa.getPosicion().posicion).remove(cosa);
			if (contenido.get(cosa.getPosicion().posicion).isEmpty()) {
				contenido.remove(cosa.getPosicion().posicion);
			}
			contenidoSincronico.remove(cosa);
		}
		aEliminar.clear();
	}
	
	public void remover (CosaDelMapa entidad)
	{
		for (VistaMapa vista : vistas) {
			vista.remover(entidad);
		}
		if (!aEliminar.contains(entidad)) {
			aEliminar.add(entidad);
		}
	}
	
	public void reposicionar (CosaDelMapa entidad, Point destino) throws PosicionFueraDeMapaException
	{
		if (fueraDeMapa(destino)) {
			throw new PosicionFueraDeMapaException();
		} else {
			contenido.get(entidad.getPosicion().posicion).remove(entidad);
			if (contenido.get(entidad.getPosicion().posicion).isEmpty()) {
				contenido.remove(entidad.getPosicion().posicion);
			}
			if (!contenido.containsKey(destino)) {
				contenido.put(destino, new LinkedList<CosaDelMapa>());
			}
			contenido.get(destino).add(entidad);
			entidad.getPosicion().posicion = destino;
		}
	}

	public boolean estaLiberado ()
	{
		for (LinkedList<CosaDelMapa> listas : contenido.values()) { 
			for (CosaDelMapa entidad : listas) {
				if (!entidad.estaLiberado()) {
					return false;
				}
			}
		}
		return true;
	}
	
	public void cicloDeJuego()
	{
		while (!termino) {
			long inicioCiclo = System.currentTimeMillis(); 
			for (CosaDelMapaSincronica entidad : contenidoSincronico) {
				entidad.recibirPulsoReloj(1.0f / pulsosPorSegundo);
			}
			this.realizarInteracciones();
			this.limpiar();
			if (contenidoSincronico.isEmpty()) {
				this.terminarJuego(false);
			} else {
				for (VistaMapa vista : vistas) {
					vista.actualizar();
				}
				try {
					long duracionCiclo = System.currentTimeMillis() - inicioCiclo;
					long duracionSleep = 1000 / pulsosPorSegundo - duracionCiclo;
					if (duracionSleep > 0) {
						Thread.sleep(duracionSleep);
					}
				} catch (InterruptedException e) {}
			}
		}
	}
	
	private void realizarInteracciones()
	{
		for (LinkedList<CosaDelMapa> balde : contenido.values()) {
			for (int i = 0; i < balde.size(); i++) {
				for (int j = i + 1; j < balde.size(); j++) {
					try {
						balde.get(i).interactuarCon(balde.get(j));
						balde.get(j).interactuarCon(balde.get(i));
					} catch (NoPasarException e) {}
				}
			}
		}
	}
	
	public void terminarJuego(boolean esGanado)
	{
		termino = true;
		ganado = esGanado;
	}
	
	public void agregarVista (VistaMapa vista)
	{
		vista.setDimensiones(maxX, maxY);
		if (!vistas.contains(vista)) {
			vistas.add(vista);
			for (LinkedList<CosaDelMapa> posicion : contenido.values()) {
				for (CosaDelMapa cosa : posicion) {
					vista.agregar(cosa);
				}
			}
		}
	}
	
	public void removerVista (CentralVista vista)
	{
		vistas.remove(vista);
	}
	
	public Element serializar()
	{
		Element raiz = new Element("Mapa");
		raiz.setAttribute("x", Integer.toString(maxX));
		raiz.setAttribute("y", Integer.toString(maxY));
		for (Map.Entry<Point, LinkedList<CosaDelMapa>> parDatos : contenido.entrySet()) {
			Point posicion = parDatos.getKey();
			LinkedList<CosaDelMapa> cosas = parDatos.getValue();
			
			Element nodo = new Element("posicion");
			nodo.setAttribute("x", Integer.toString(posicion.x));
			nodo.setAttribute("y", Integer.toString(posicion.y));
			for (CosaDelMapa cosa : cosas) {
				Element nodoCosa = cosa.serializar();
				nodo.addContent(nodoCosa);
			}
			raiz.addContent(nodo);
		}
		return raiz;
	}
}
